package data;
import java.util.ArrayList;
import java.io.PrintStream;
import static java.util.Arrays.sort;
import static conf.Configuration.*;

/**
 * The course data for a new course. Each course stores its name, cap, gender balance information, 
 * a list of potential students, and the students that are actually enrolled.<br />
 * It will also contain a resolve() method, which takes the list of potential students and calculates which 
 * students will generate the <i>best possible score for this class.</i> These students are then added to 
 * the array of actually enrolled students.
 * @author Frans Honig, Maxime Serrano, Raphael Townshend
 */
public class Course 
{
	/**
	 * Is it the first resolution?
	 */
	private boolean firstResolve = true;
	
	/**
	 * Course's name. Also contains the ID in characters 2 and 3.
	 */
	private final String COURSE_NAME;

	/**
	 * What the maximum number of students in the course will be.
	 */
	private final int COURSE_CAP;

	/**
	 * Whether or not the teacher has requested gender balance for this course.
	 */
	private final boolean GENDER_BALANCE_REQUESTED;

	/**
	 * The array of students that are actually enrolled in this class.
	 */
	public Student[] studentsEnrolled;

	/**
	 * The list of students that might potentially get enrolled in this class (i.e. that have this as a choice).
	 */
	private ArrayList<Student> potentialStudents = new ArrayList<Student>(); 

	/**
	 * The number of students enrolled in the course.
	 */
	private int numStudents = 0;
	
	/**
	 * The course's ID number, following the KS in the data.
	 */
	private final int ID;

	/**
	 * Creates a new course with the given information.
	 * @param courseName The course's name.
	 * @param courseCap The maximum number of students in the course.
	 * @param genderBalance Whether or not the teacher has requested gender balance in their course.
	 */
	public Course(final String courseName, final int courseCap, final boolean genderBalance)
	{
		if (DEBUG)
		{	
			System.out.println("Constructor for class " + courseName);
		}
		COURSE_NAME = courseName;
		COURSE_CAP = courseCap;
		GENDER_BALANCE_REQUESTED = genderBalance;
		studentsEnrolled = new Student[courseCap];
		
		// If this throws an exception, then our data isn't formatted properly.
		ID = Integer.parseInt(COURSE_NAME.substring(2, 4));
	}

	/**
	 * This method takes in a student and adds it to the list of potential students in this course.
	 * @param student The student to add to the course.
	 * @return Whether or not the addition was successful.
	 */
	public boolean addPotentialStudent(final Student student)
	{
		boolean success = potentialStudents.add(student);
		return success;
	}
	/**
	 * This method takes in a student and removes it from the list if possible.
	 * @param student The student to be removed.
	 * @return Whether or not the removal was successful.
	 */
	public boolean removePotentialStudent(final Student student)
	{
		boolean success = potentialStudents.contains(student);
		potentialStudents.remove(student);
		return success;
	}

	/**
	 * Generates the course's ID.
	 * @return The ID of the class.
	 */
	public int getID()
	{
		return ID;
	}
	
	public String getName()
	{
		return COURSE_NAME;
	}

	/**
	 * Turns the course into a printable object.
	 * @return A String description of the object.
	 */
	public String toString()
	{
		String var = "[Course] " + COURSE_NAME + " Cap: " + COURSE_CAP + " gender balanced: " + GENDER_BALANCE_REQUESTED;
		var += ", full: " + (!(isNotFull()));
		return var;
	}

	/**
	 * Enrolls a student in the course.
	 * @param s A student to enroll.
	 */
	private void enrollStudent(final Student s)
	{
		if (numStudents < COURSE_CAP && !(s.isPlaced()))
		{
			if(this.getID() == 40)
			{
				System.out.println(numStudents);
			}
			if(s.data.studentName.equals("Jeffrey Kuan"))
			{
				System.out.println(getID());
			}
			studentsEnrolled[numStudents] = s;
			numStudents++;
			s.place(ID, numStudents - 1);
			removePotentialStudent(s);
		}
	}
	
	/**
	 * 
	 * @return
	 */
	public boolean isNotFull()
	{
		return numStudents != COURSE_CAP;
	}
	
	/**
	 * Tries to force the given student to be enrolled. Shouldn't be called until the very end.
	 * @param s The student to add.
	 */
	public void tryForceEnroll(final Student s)
	{
		if (numStudents < COURSE_CAP && !(s.isPlaced()))
		{
			if(this.getID() == 40)
			{
				System.out.println("fs" + numStudents);
			}
			if(s.data.studentName.equals("Jeffrey Kuan"))
			{
				System.out.println(getID());
			}
			
			studentsEnrolled[numStudents] = s;
			numStudents++;
			s.place(ID, numStudents - 1);
		}
	}

	/**
	 * Resolves the class for the Nth time. Adds all the appropriate peoples to the class.<br />
	 * Does not currently take into account gender balance.
	 */
	public void resolve()
	{
		if (DEBUG)
		{
			System.out.println("resolve() for class " + COURSE_NAME);
		}
	
		final int N = COURSE_CAP - numStudents;
		double[] desirabilityScores = new double[potentialStudents.size()];
		generateDesirabilityScores(desirabilityScores);

		// Gets top N students
		Student[] topNStudents = new Student[N];
		double[] tempDesirabilityScores = copy(desirabilityScores);
		getTopNStudents(N, topNStudents, tempDesirabilityScores, desirabilityScores);
		
		// Enrolls them
		enrollStudents(topNStudents);
		
		// If we're full, remove all picks for this class.
		if (numStudents == COURSE_CAP)
		{
			removePotentialStudents();
		}
		firstResolve = false;
	}

	/**
	 * Returns a copy of the input array.
	 * @param array The input array to copy.
	 * @return The copy of the array.
	 */
	private double[] copy(final double[] array)
	{
		double[] copy = new double[array.length];
		for (int i = 0; i < copy.length; i++)
		{
			copy[i] = array[i];
		}
		return copy;
	}

	/**
	 * Removes value at index from the array, returning the result.
	 * @param array The array to remove from. Never actually changed.
	 * @param index The index to use.
	 * @return The modified array.
	 */
	private double[] removeByIndex(final double[] array, final int index)
	{
		if (index < array.length)
		{
			double[] temp = copy(array);
			for (int i = index; i < array.length - 1; i++)
			{
				temp[index] = temp[index + 1];
			}

			double[] returnVal = new double[temp.length - 1];
			for (int i = 0; i < returnVal.length; i++)
			{
				returnVal[i] = temp[i];
			}

			return returnVal;
		}
		return null;
	}

	/**
	 * Prints the class data. Used when outputting a solution.
	 * @param out The PrintStream to print to. Usually System.out or a file output stream.
	 */
	public void print(final PrintStream out)
	{
		out.println(this);
		int num = 0;
		
		for (Student s : studentsEnrolled)
		{
			if(ID == 47)
			System.out.println(s);
			if (s != null)
			{
				out.println("\t" + s);
				num++;
			}
		}
		if (num == 0)
		{
			out.println("\tEmpty.");
		}
	}

	/**
	 * Implements sequential search: looks for <code>key</code> in <code>array</code>, returning
	 * the first index of <code>key</code> in <code>array</code>. If it is not found, returns -1.
	 * @param array The array to search in.
	 * @param key The object to look for.
	 * @return The index of key in array.
	 */
	private int search(final double[] array, final double key)
	{
		for (int i = 0; i < array.length; i++)
		{
			if (array[i] == key)
			{
				return i;
			}
		}
		return -1;
	}

	/**
	 * Calculates the heuristic for the class.
	 * @return The heuristic value for this class (sum of all student heuristics).
	 */
	public int calcHeuristic()
	{
		if (DEBUG)
		{
			System.out.println("calcHeuristic() for class " + COURSE_NAME);
		}
		int heuristic = 0;
		int weird = 0;
		for (Student s : studentsEnrolled)
		{
			if (s != null)
			{
				if (s.calcHeuristicScore(ID) == 900000)
				{
					weird++;
				}
				if (s.hasSecondChoiceForced())
				{
					System.out.println(s);
				}
				
				heuristic += s.calcHeuristicScore(ID);
			}
		}
		//System.out.println("Course: " + COURSE_NAME + " has " + weird + " weird people out of " + studentsEnrolled.length + " maximum.");
		return heuristic;
	}

	/**
	 * Calculates the choice numbers for this course.
	 * @param choice The choice number to calculate for.
	 * @return The number of students enrolled in this course who got choice number <code>choice</code>.
	 */
	public int calcChoices(final int choice)
	{
		if (DEBUG)
		{
			System.out.println("calcChoices() for class " + COURSE_NAME);
		}
		int num = 0;
		for (Student s : studentsEnrolled)
		{
			if (s != null && s.courseIsChoice(ID, choice))
			{
				num++;
			}
		}
		return num;
	}
	
	/**
	 * Removes the course from the given list.
	 * @param listToRemoveFrom
	 */
	public void removeCourse(final ArrayList<Course> listToRemoveFrom)
	{
		if (DEBUG)
		{
			System.out.println("removeCourse() for class " + COURSE_NAME);
		}
		
		listToRemoveFrom.remove(this);
		for (Student s : studentsEnrolled)
		{
			if (s != null)
			{
				s.reset();
			}
		}
	}
	
	/**
	 * Deletes all potential students.
	 */
	public void removePotentialStudents()
	{
		if (DEBUG)
		{
			System.out.println("removePotentialStudents() for class " + COURSE_NAME);
		}
		
		for (Student s : potentialStudents)
		{
			s.removePick(ID);
		}
	}
	
	/**
	 * Whether or not the class has few enough students that it needs to be deleted.
	 * @return The result.
	 */
	public boolean shouldBeDeleted()
	{
		return numStudents < MIN_STUDENTS_PER_COURSE;
	}
	
	/**
	 * Enrolls all student teachers.
	 */
	public void enrollTeachers()
	{
		if (DEBUG)
		{	
			System.out.println("enrollTeachers() for class " + COURSE_NAME);		
		}
		ArrayList<Student> sTeachers = new ArrayList<Student>();
		
		for (Student s : potentialStudents)
		{
			if(s.isTeacher() && s.topPick(ID))
			{
				sTeachers.add(s);
			}
		}
		
		for (Student s : sTeachers)
		{
			enrollStudent(s);
		}
	}
	
	public void enrollForcedLatePeople()
	{
		ArrayList<Student> sForcedLate = new ArrayList<Student>();
		for (Student s : potentialStudents)
		{
			if(s.isForcedLate())
			{
				sForcedLate.add(s);
			}
		}
		
		for (Student s : sForcedLate)
		{
			enrollStudent(s);
		}
	}
	
	private void generateDesirabilityScores(double[] desirabilityScores)
	{
		for (int i = 0; i < desirabilityScores.length; i++)
		{
			// transform would be here if necessary
			if (potentialStudents.get(i).isPlaced())
			{
				desirabilityScores[i] = -1;
			}
			else
			{
				desirabilityScores[i] = potentialStudents.get(i).getDesirabilityScore(ID);
			}
		}
	}
	
	private void getTopNStudents(final int N, Student[] topNStudents, double[] tempDesirabilityScores, double[] desirabilityScores)
	{
		sort(tempDesirabilityScores);
		for (int i = 0, currStudent = 0; i < N; i++)
		{
			if (tempDesirabilityScores.length - 1 - i > -1)
			{
				int studentIndex = search(desirabilityScores, tempDesirabilityScores[tempDesirabilityScores.length - 1 - i]);
				if (studentIndex != -1)
				{
					topNStudents[currStudent] = potentialStudents.get(studentIndex);
					currStudent++;
					desirabilityScores = removeByIndex(desirabilityScores, studentIndex);
				}
			}
		}
	}
	
	private void enrollStudents(Student[] topNStudents)
	{
		if (firstResolve)
		{
			// Iterates through the first time
			for (Student s : topNStudents)
			{
				if (s != null)
				{
					if (s.topPick(getID()))
					{
						enrollStudent(s);
					}
				}
			}
		}
		else
		{	
			for (Student s : topNStudents)
			{
				if (s != null)
				{
					if (s.eligible(ID))
					{
						enrollStudent(s);
					}
				}
			}
		}
	}
	
	public void unenroll(Student s)
	{
		for (int i = 0; i < numStudents; i++)
		{
			if(studentsEnrolled[i].equals(s))
			{
				studentsEnrolled[i] = null;
				for (int j = i; j < numStudents - 1; j++)
				{
					studentsEnrolled[j] = studentsEnrolled[j + 1];
				}
				if (numStudents < studentsEnrolled.length)
				{
					studentsEnrolled[numStudents] = null;
				}
				else if (studentsEnrolled[studentsEnrolled.length - 1] != null && studentsEnrolled[studentsEnrolled.length - 1].equals(studentsEnrolled[studentsEnrolled.length - 2]))
				{
					studentsEnrolled[studentsEnrolled.length - 1] = null;
				}
				numStudents--;
				return;
			}
		}
	}
}
